﻿''' <summary>
''' The primary GdiGaming API component responsible for executing the game loop and managing the game.
''' </summary>
''' <remarks></remarks>
Public Class GameEngine
    ' there may later be a need for ViewPort functionality but for now no clip area is needed...
    'Friend Shared InvalidateCanvasRectangle As New Rectangle(-1, -1, 0, 0)

    Protected _EngineTask As System.Threading.Tasks.Task

    ''' <summary>
    ''' Occurs once per frame after the frame has been drawn and any wait time has elapsed.
    ''' </summary>
    ''' <remarks></remarks>
    Public Event FrameComplete As EventHandler(Of GameEngineEventArgs)
    ''' <summary>
    ''' Occurs once per frame after all drawing but before any wait time.
    ''' </summary>
    ''' <remarks></remarks>
    Public Event GameLoop As EventHandler(Of GameEngineEventArgs)
    ''' <summary>
    ''' Occurs when an active game is paused.
    ''' </summary>
    ''' <remarks></remarks>
    Public Event GamePaused As EventHandler(Of GameEngineEventArgs)
    ''' <summary>
    ''' Occurs when a paused game is resumed.
    ''' </summary>
    ''' <remarks></remarks>
    Public Event GameResumed As EventHandler(Of GameEngineEventArgs)
    ''' <summary>
    ''' Occurs when a stopped game is started.
    ''' </summary>
    ''' <remarks></remarks>
    Public Event GameStarted As EventHandler(Of GameEngineEventArgs)
    ''' <summary>
    ''' Occurs when the current frame is finished, after a call to EndGame.
    ''' </summary>
    ''' <remarks></remarks>
    Public Event GameStopped As EventHandler(Of GameEngineEventArgs)

    Protected _Audio As GameAudio = New GameAudio(Me)
    ''' <summary>
    ''' Gets the active GameAudio instance for this game, which provides access to playing sounds and music.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The current GameAudio instance for this game.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property Audio As GameAudio
        Get
            Return _Audio
        End Get
    End Property

    Protected _Canvas As RenderCanvas
    ''' <summary>
    ''' Gets or sets the RenderCanvas control responsible for drawing the GameEngine image buffers.
    ''' </summary>
    ''' <value>Sets the RenderCanvas to be used by the GameEngine.</value>
    ''' <returns>Gets the RenderCanvas used by the GameEngine.</returns>
    ''' <remarks></remarks>
    Public Property Canvas() As RenderCanvas
        Get
            Return _Canvas
        End Get
        Set(ByVal value As RenderCanvas)
            If value IsNot _Canvas Then
                If _Canvas IsNot Nothing Then
                    _Canvas._Engine = Nothing
                End If
                _Canvas = value
                If _Canvas IsNot Nothing Then
                    _Canvas._Engine = Me
                End If
            End If
        End Set
    End Property

    Protected _ContainerControl As ContainerControl
    ''' <summary>
    ''' Gets or sets the parent control (typically a Form) hosting this GameEngine.
    ''' </summary>
    ''' <value>Sets the parent control for the GameEngine.</value>
    ''' <returns>Gets the parent control of the GameEngine.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Property ContainerControl() As ContainerControl
        Get
            Return Me._ContainerControl
        End Get
        Set(ByVal value As ContainerControl)
            Me._ContainerControl = value
        End Set
    End Property

    ''' <summary>
    ''' Gets the GameEngines current GameScene instance specified by the CurrentSceneName property value.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The current GameScene instance, or Nothing (Null) if the CurrentSceneName property is not set.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property CurrentScene As GameScene
        Get
            If _CurrentSceneName IsNot Nothing AndAlso _Scenes.Contains(_CurrentSceneName) Then
                Return _Scenes(_CurrentSceneName)
            End If
            Return Nothing
        End Get
    End Property

    Private _CurrentSceneName As String = String.Empty
    ''' <summary>
    ''' The name of the active GameScene for this game. Must be the name of a GameScene added to
    ''' the GameEngine.Scenes collection.
    ''' </summary>
    ''' <value>Sets the name of the current GameScene.</value>
    ''' <returns>Gets the name of the current GameScene.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public Property CurrentSceneName() As String
        Get
            Return _CurrentSceneName
        End Get
        Set(ByVal value As String)
            If value Is Nothing Then value = String.Empty
            If value.Length > 0 Then
                If Not _CurrentSceneName = value Then
                    If _Scenes.Contains(value) Then
                        _CurrentSceneName = value
                    Else
                        Throw New ArgumentException("A scene with the specified name was not found in the engine's Scenes collection.")
                    End If
                End If
            Else
                _CurrentSceneName = value
            End If
        End Set
    End Property

    Protected Friend _EngineState As GameEngineState
    ''' <summary>
    ''' Gets the operational state of the GameEngine.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The operational state of the GameEngine.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property EngineState As GameEngineState
        Get
            Return _EngineState
        End Get
    End Property

    Protected _Factory As New ResourceFactory(Me)
    Public ReadOnly Property Factory As ResourceFactory
        Get
            Return _Factory
        End Get
    End Property

    Protected _FrameRate As Single
    ''' <summary>
    ''' Gets the actual frame rate for the game as of the last frame.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The game frame rate as of the last frame.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property FrameRate As Single
        Get
            Return _FrameRate
        End Get
    End Property

    Protected _IsEndGamePending As Boolean
    ''' <summary>
    ''' Indicates that the EndGame method has been called, but execution of the current game loop iteration has not yet completed.
    ''' </summary>
    ''' <value></value>
    ''' <returns>True if the game is ending, otherwise false.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property IsEndGamePending As Boolean
        Get
            Return _IsEndGamePending
        End Get
    End Property

    ''' <summary>
    ''' Indicates if the game is currently running at less that 80% of the target frame rate.
    ''' </summary>
    ''' <value></value>
    ''' <returns>True if the game is running slowly, otherwise false.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property IsRunningSlow As Boolean
        Get
            Return _FrameRate < 0.8F * _TargetFrameRate
        End Get
    End Property

    Protected _Input As New GameInput
    ''' <summary>
    ''' Gets the active GameInput instance for this GameEngine, providing access to mouse and keyboard input information.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The current GameInput instance for this GameEngine.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property Input As GameInput
        Get
            Return _Input
        End Get
    End Property

    Protected _MinimumFrameTime As Single = 0.0333333351F
    Protected _TargetFrameRate As Single = 30.0F
    ''' <summary>
    ''' Gets or sets the desired frame rate. This value should be left at 30.0.
    ''' </summary>
    ''' <value>Sets the target frame rate.</value>
    ''' <returns>Gets the target frame rate.</returns>
    ''' <remarks></remarks>
    Public Property TargetFrameRate() As Single
        Get
            Return _TargetFrameRate
        End Get
        Set(ByVal value As Single)
            _TargetFrameRate = value
            _MinimumFrameTime = 1 / _TargetFrameRate
        End Set
    End Property

    Protected _Resources As New AssetManager
    ''' <summary>
    ''' Gets the active AssetManager for this GameEngine. The AssetManager handles loading and caching
    ''' resources. Assets should normally be requested from the CurrentScene and not from the AssetManager
    ''' directly. Use the AssetManager to manually add assets to the game at run-time.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The active AssetManager instance for this GameEngine.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property Resources As AssetManager
        Get
            Return _Resources
        End Get
    End Property

    Protected _Scenes As New GameEngineSceneCollection(Me)
    ''' <summary>
    ''' Contains the collection of GameScene objects which make up this game.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The collection of GameScene objects which make up this game.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property Scenes As GameEngineSceneCollection
        Get
            Return _Scenes
        End Get
    End Property

    Protected _Time As New GameTime
    ''' <summary>
    ''' Gets the active GameTime instance for this GameEngine, providing access to the last frame time
    ''' as well as elapsed real time and elapsed game time.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The active GameTime instance for this GameEngine.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)>
    Public ReadOnly Property Time As GameTime
        Get
            Return _Time
        End Get
    End Property

    <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)>
    Public Overrides Property Site() As System.ComponentModel.ISite
        Get
            Return MyBase.Site
        End Get
        Set(ByVal value As System.ComponentModel.ISite)
            MyBase.Site = value
            If value IsNot Nothing Then
                Dim host As System.ComponentModel.Design.IDesignerHost
                host = value.GetService(GetType(System.ComponentModel.Design.IDesignerHost))
                If host IsNot Nothing Then
                    Me._ContainerControl = CType(host.RootComponent, ContainerControl)
                Else
                    Me._ContainerControl = Nothing
                End If
            End If
        End Set
    End Property

    ''' <summary>
    ''' Signals the current game to end.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub EndGame()
        _IsEndGamePending = True
    End Sub

    ''' <summary>
    ''' Pauses the running game.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub PauseGame()
        OnGamePause(New GameEngineEventArgs(Me, _Time))
        _EngineState = GameEngineState.Paused
    End Sub

    ''' <summary>
    ''' Starts the game from the beginning if it is stopped, or unpauses the game if it is paused.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub StartGame()
        If _Canvas Is Nothing Then
            Throw New InvalidOperationException("The GameEngine.Canvas property has not been set to an instance of RenderCanvas. The GameEngine cannot start without a RenderCanvas.")
        End If
        If _EngineState = GameEngineState.Stopped Then
            _Resources.LoadInitialAssets()
            _EngineTask = New System.Threading.Tasks.Task(AddressOf DoGameLoop, Threading.Tasks.TaskCreationOptions.LongRunning)
            _IsEndGamePending = False
            _Time.Start()
            _EngineTask.Start()
        End If
        _Canvas.Focus()
        OnGameStart(New GameEngineEventArgs(Me, _Time))
    End Sub

    Protected Sub CheckMouseButton(item As GameObject, updateArgs As GameEngineEventArgs, button As MouseButtons)
        If Input.CurrentMouseState(button) Then
            If Not Input.PreviousMouseState(button) Then
                item.OnMouseDown(updateArgs)
            End If
        Else
            If Input.PreviousMouseState(button) Then
                item.OnMouseUp(updateArgs)
            End If
        End If
    End Sub

    Protected Overridable Sub DoGameLoop()
        'In order to achieve a synchronous drawing of the canvas, we
        ' use a custom routine to paint one of two back-buffer images.
        Dim invalidateCanvas As New Action(AddressOf _Canvas.UpdateFrame)

        Dim index As Integer
        While Not _IsEndGamePending
            If _EngineState = GameEngineState.Started Then
                If _Audio.IsPaused Then _Audio.ResumeAll()

                Dim scene As GameScene = CurrentScene
                If scene IsNot Nothing Then
                    Dim updateArgs As New GameEngineEventArgs(Me, _Time)

                    If Not scene.IsLoaded Then scene.OnLoad(updateArgs)
                    Dim objects() As GameObject = scene.GetSceneObjects()
                    Dim loopTimer As New System.Diagnostics.Stopwatch
                    loopTimer.Start()

                    '1) Update Input
                    SyncLock _Input
                        _Input.OnUpdate(updateArgs)
                    End SyncLock

                    For index = 0 To objects.Length - 1
                        Dim item As GameObject = objects(index)
                        If Not item.Enabled Then Continue For
                        If Not item.IsLoaded Then item.OnLoad(updateArgs)

                        If item.ProcessInput Then
                            item.OnInput(updateArgs)
                            Dim bounds As RectangleF = item.GetRectangleCollider
                            If bounds.Contains(Input.CurrentMouseState.MouseLocation) Then
                                CheckMouseButton(item, updateArgs, MouseButtons.Left)
                                CheckMouseButton(item, updateArgs, MouseButtons.Right)
                                CheckMouseButton(item, updateArgs, MouseButtons.Middle)
                                CheckMouseButton(item, updateArgs, MouseButtons.XButton1)
                                CheckMouseButton(item, updateArgs, MouseButtons.XButton2)
                            End If
                        End If
                    Next

                    '1.A) Update Hud Element Input
                    Dim hudElements As New List(Of GameObject)
                    For Each layer In scene.HudLayers.OrderBy(Of Integer)(Function(hl) hl.ZOrder)
                        If layer.Visible Then
                            hudElements.AddRange(layer.GetElements)
                        End If
                    Next
                    For index = 0 To hudElements.Count - 1
                        Dim item As GameObject = hudElements(index)
                        If Not item.Enabled Then Continue For
                        If Not item.IsLoaded Then item.OnLoad(updateArgs)

                        If item.ProcessInput Then
                            item.OnInput(updateArgs)
                            Dim bounds As RectangleF = item.GetRectangleCollider
                            If bounds.Contains(Input.CurrentMouseState.MouseLocation) Then
                                CheckMouseButton(item, updateArgs, MouseButtons.Left)
                                CheckMouseButton(item, updateArgs, MouseButtons.Right)
                                CheckMouseButton(item, updateArgs, MouseButtons.Middle)
                                CheckMouseButton(item, updateArgs, MouseButtons.XButton1)
                                CheckMouseButton(item, updateArgs, MouseButtons.XButton2)
                            End If
                        End If
                    Next

                    '2) Update each Background
                    Dim backgrounds() As GameBackground = scene.GetSceneBackgrounds
                    For index = 0 To backgrounds.Length - 1
                        Dim background As GameBackground = backgrounds(index)
                        If Not background.IsLoaded Then background.OnLoad(updateArgs)
                        If background.Enabled Then
                            background.OnUpdate(updateArgs)
                        End If
                    Next

                    '3) Update each GameObject
                    '   1) Call OnUpdate
                    '   2) Check Collision
                    If objects.Length > 0 Then
                        Dim collidersList As New List(Of GameObject)(objects)
                        Dim collisionList As New List(Of GameObject)

                        For index = 0 To objects.Length - 1
                            Dim item As GameObject = objects(index)
                            If item.Enabled Then
                                If Not item.IsCollider Then collidersList.Remove(item)
                                If item.HasCollision Then collisionList.Add(item)
                                item.OnUpdate(updateArgs)
                            Else
                                collidersList.Remove(item)
                            End If
                        Next

                        For index = collisionList.Count - 1 To 0 Step -1
                            Dim item As GameObject = collisionList(index)
                            collisionList.Remove(item)
                            Dim hadCollision As Boolean

                            If item.UseRectangleCollider Then
                                Dim thisCollider As RectangleF = item.GetRectangleCollider
                                For otherindex As Integer = 0 To collidersList.Count - 1
                                    Dim other As GameObject = collidersList(otherindex)
                                    If item Is other Then Continue For
                                    Dim otherCollider As RectangleF = other.GetRectangleCollider
                                    hadCollision = thisCollider.IntersectsWith(otherCollider)
                                    If hadCollision Then
                                        Dim collisionArgs As CollisionEventArgs
                                        collisionArgs = New CollisionEventArgs(updateArgs, other)
                                        item.OnCollision(collisionArgs)
                                        If other.HasCollision Then
                                            collisionArgs = New CollisionEventArgs(updateArgs, item)
                                            other.OnCollision(collisionArgs)
                                        End If
                                    End If
                                Next
                            Else
                                Dim thisCollider As Circle = item.GetCircleCollider
                                For otherindex As Integer = 0 To collidersList.Count - 1
                                    Dim other As GameObject = collidersList(otherindex)
                                    If item Is other Then Continue For
                                    Dim otherCollider As Circle = other.GetCircleCollider
                                    hadCollision = thisCollider.IntersectsWith(otherCollider)
                                    If hadCollision Then
                                        Dim collisionArgs As CollisionEventArgs
                                        collisionArgs = New CollisionEventArgs(updateArgs, other)
                                        item.OnCollision(collisionArgs)
                                        If other.HasCollision Then
                                            collisionArgs = New CollisionEventArgs(updateArgs, item)
                                            other.OnCollision(collisionArgs)
                                        End If
                                    End If
                                Next
                            End If

                        Next
                    End If
                    '3.A) Update Hud Elements
                    For index = 0 To hudElements.Count - 1
                        Dim item As GameObject = hudElements(index)
                        If Not item.Enabled Then Continue For
                        item.OnUpdate(updateArgs)
                    Next

                    '3.B) Update Scene
                    scene.OnUpdate(updateArgs)

                    '4) Draw Game
                    '   1) Render each Background
                    '   2) Render each GameObject
                    '   3) Render each HUD Object
                    Dim visibleBounds As Rectangle = _Canvas.DisplayRectangle
                    If scene IsNot Nothing Then
                        index = 0
                        Dim buffer As Image = _Canvas.GetDrawBuffer
                        Dim gfx As Graphics = Graphics.FromImage(buffer)
                        gfx.Clear(Canvas.BackColor)

                        Dim frameArgs As New DrawEventArgs(Me, Time, New PaintEventArgs(gfx, visibleBounds))
                        '   1) Render each Background
                        For index = 0 To backgrounds.Length - 1
                            Dim background As GameBackground = backgrounds(index)
                            If background.Enabled AndAlso background.Visible Then
                                background.OnDraw(frameArgs)
                            End If
                        Next
                        '   2) Render each GameObject
                        For index = 0 To objects.Length - 1
                            Dim item As GameObject = objects(index)
                            If item.Enabled AndAlso item.Visible Then
                                If visibleBounds.Contains(item.Position.ToPoint) Then
                                    item.OnDraw(frameArgs)
                                End If
                            End If
                        Next
                        scene.OnPreHudDraw(frameArgs)
                        '   3) Render Hud Elements
                        For index = 0 To hudElements.Count - 1
                            Dim item As GameObject = hudElements(index)
                            If item.Enabled AndAlso item.Visible Then
                                item.OnDraw(frameArgs)
                            End If
                        Next
                        '   4) Render Scene (Optional Drawing)
                        scene.OnDraw(frameArgs)
                        buffer = Nothing
                        gfx.Dispose()
                    End If
                    If _ContainerControl IsNot Nothing AndAlso _ContainerControl.InvokeRequired Then
                        _ContainerControl.Invoke(invalidateCanvas)
                    Else
                        invalidateCanvas()
                    End If

                    '5) Update Sounds
                    _Audio.OnUpdate(updateArgs)

                    '6) Raise GameLoop event for containing control
                    Dim loopDelegate As New Action(Of GameEngineEventArgs)(AddressOf OnGameLoop)
                    If _ContainerControl IsNot Nothing AndAlso _ContainerControl.InvokeRequired Then
                        _ContainerControl.Invoke(loopDelegate, updateArgs)
                    Else
                        loopDelegate(updateArgs)
                    End If

                    '7) Wait for any time remaining in this frame
                    While loopTimer.Elapsed.TotalSeconds < _MinimumFrameTime
                        System.Threading.Thread.SpinWait(10)
                    End While

                    '8) Update the game time and frame rate
                    loopTimer.Stop()
                    Dim elapsedTime As Single = CSng(loopTimer.Elapsed.TotalSeconds)
                    _FrameRate = Geometry.ONE / elapsedTime
                    _Time.Update(elapsedTime)

                    '9) Raise FrameComplete event for containing control
                    Dim frameDelegate As New Action(Of GameEngineEventArgs)(AddressOf OnFrameComplete)
                    If _ContainerControl IsNot Nothing AndAlso _ContainerControl.InvokeRequired Then
                        _ContainerControl.Invoke(frameDelegate, updateArgs)
                    Else
                        frameDelegate(updateArgs)
                    End If

                    '10) Restart Game Loop...
                Else
                    'No Scene to render; safe to wait a while since a new scene will have
                    'initialization cost anyway (e.g. it should be coded to handle an initial load time).
                    System.Threading.Thread.Sleep(100)
                End If
            Else
                If Not _Audio.IsPaused Then Audio.PauseAll()
                'Engine is paused; just wait a short moment so that un-pausing is immediate to the user.
                System.Threading.Thread.Sleep(10)
            End If
        End While
        _Audio.StopAll()
        'Engine is finished, notify containing control by raising the GameStopped event
        Dim alertDelegate As New Action(Of GameEngineEventArgs)(AddressOf OnGameStopped)
        If _ContainerControl IsNot Nothing AndAlso _ContainerControl.InvokeRequired Then
            _ContainerControl.BeginInvoke(alertDelegate, New GameEngineEventArgs(Me, _Time))
        Else
            alertDelegate.BeginInvoke(New GameEngineEventArgs(Me, _Time), Nothing, Me)
        End If
    End Sub

    Protected Overridable Sub OnFrameComplete(ByVal e As GameEngineEventArgs)
        RaiseEvent FrameComplete(Me, e)
    End Sub

    Protected Overridable Sub OnGameLoop(ByVal e As GameEngineEventArgs)
        RaiseEvent GameLoop(Me, e)
    End Sub

    Protected Overridable Sub OnGamePause(ByVal e As GameEngineEventArgs)
        RaiseEvent GamePaused(Me, e)
    End Sub

    Protected Overridable Sub OnGameResumed(e As GameEngineEventArgs)
        RaiseEvent GameResumed(Me, e)
    End Sub

    Protected Overridable Sub OnGameStart(ByVal e As GameEngineEventArgs)
        If _EngineState = GameEngineState.Paused Then
            OnGameResumed(e)
        End If
        _EngineState = GameEngineState.Started
        RaiseEvent GameStarted(Me, e)
    End Sub

    Protected Overridable Sub OnGameStopped(ByVal e As GameEngineEventArgs)
        _EngineState = GameEngineState.Stopped
        _EngineTask.Wait()
        _EngineTask.Dispose()
        _EngineTask = Nothing
        Dim images() As Bitmap = _Resources._CachedResources.GetValues(Of Bitmap)()
        _Resources._CachedResources.ClearAll()
        For Each bmp As Bitmap In images
            bmp.Dispose()
        Next
        RaiseEvent GameStopped(Me, e)
    End Sub

End Class